﻿using System;
using System.Data;
using System.Text;
using System.Collections;
using System.Collections.Generic;

using BizElements.Core.ProviderComponents;

namespace BizElements.Core
{
    #region Documentation.
    /// <summary>
    /// Contains database table/view metadata and methods.
    /// Factory class for related IEntity, EntityCollection and ADO.NET objects.
    /// </summary>
    /// <remarks><para>Provides methods for the creation of the compatibile O/RM objects
    /// and contains metadata for the table/view it represents such as:
    /// <list type="bullet">
    ///	<item>Catalog data to which to which the table belongs to.</item>
    ///	<item>Table name and optional alias.</item>
    ///	<item>Fields owned by the table.</item>
    ///	<item>Primary key data.</item>
    ///	<item>Foreign keys.</item>
    /// </list>
    /// </para>
    /// <para><b>SealedDbTable</b> class implements <see cref="IDbTable"/> interface in a more efficient manner than 
    /// obsoleted <b>DbTable</b> class. It implements only setters mandated by <see cref="IDbTable"/> interface. All other 
    /// properties are read-only and guaranted that no attempt will be made to modify underlying data stored in
    /// <see cref="IDbTableConfiguration"/> object. This makes it possible to use a single static <b>IDbTableConfiguration</b>
    /// object that will be used by all <b>SealedDbTable</b> instances which represent the same database table/view.
    /// Latest version of <b>BizElements Generator</b> implements code generation that supports the described pattern
    /// which significantly optimizes memory usage.</para>
    /// <seealso cref="IDbColumn"/><seealso cref="DbRelation"/><seealso cref="RelationBucket"/>
    /// </remarks>	
    /// <example>
    /// The following example demonstrates how to use BizElements metadata classes to create ADO.NET DataColumn,
    /// DataTable, DataRelation, DataSet and Constraint objects.
    /// <code>
    /// EmployeesMeta employees = new EmployeesMeta();
    /// EmployeeTerritoriesMeta empTerritories = new EmployeeTerritoriesMeta();
    /// TerritoriesMeta territories = new TerritoriesMeta();
    /// RegionMeta regions = new RegionMeta();
    /// 
    /// // Create relations between tables.
    /// RelationBucket relations = new RelationBucket();
    /// relations.Add(employees, empTerritories, empTerritories.EmployeeID);
    /// relations.Add(territories, empTerritories, empTerritories.TerritoryID);
    /// relations.Add(regions, territories, territories.RegionID);
    /// 
    /// // Create a DataSet object with relations between DataTables and constraints 
    /// // (primary key, foreign key and unique constraints.
    /// DataSet ds = relations.NewDataSet("EmployeeData", true, true, true);
    /// </code>
    /// </example>
    #endregion
    [Serializable]
    public abstract class SealedDbTable : IDbTable
    {
        #region SealedDbTableMembers.

        private sealed class SealedDbTableMembers
        {
            // Created from immutableProperties.ColumnConfiguration on first access.
            public DbColumnCollection Columns;
            public DbColumnCollection PrimaryKey;
            // Created by derived classes on first access.
            public DbRelation[] ForeignKeys;
        }

        #endregion

        #region Members. Some are immutable, some are stored in a dedicated class constructed on first usage.

        IDbTableConfiguration immutableProperties;
        // Created on first usage.
        [NonSerialized]
        SealedDbTableMembers members;
        // Mutable but read-only.
        readonly string alias;
        readonly bool columnAliasesArePrefixed;

        #endregion

        #region CTor.

        /// <summary>
        /// Initializes a new instance of the <b>SealedDbTable</b> class.
        /// </summary>
        /// <param name="immutableProperties">Provides values for immutable members. Null is not allowed.</param>
        /// <param name="alias">Object alias. If <b>null</b> then it will be equal to table name.</param>
        /// <param name="columnAliasesArePrefixed">Specifies whether columns' aliases are prefixed with a table alias.</param>
        protected SealedDbTable(IDbTableConfiguration immutableProperties, string alias, bool columnAliasesArePrefixed)
        {
            if (immutableProperties == null)
                throw new ArgumentNullException("immutableProperties");

            this.immutableProperties = immutableProperties;
            this.alias = alias;
            this.columnAliasesArePrefixed = columnAliasesArePrefixed;
            this.members = new SealedDbTableMembers();
        }

        #endregion

        #region Initialization and post-initialization.

        private void EnsureColumnsAreCreated()
        {
            if (this.members.Columns == null)
            {
                lock (this.members)
                {
                    this.members.Columns = new DbColumnCollection(this.immutableProperties.Columns.Length);
                    foreach (IDbColumnConfiguration cfg in this.immutableProperties.Columns)
                        this.members.Columns.Add(new SealedDbColumn(cfg, this));

                    this.members.Columns.MakeReadOnly();
                }
            }
        }

        private void EnsurePrimaryKeyIsCreated()
        {
            if (this.members.PrimaryKey == null)
            {
                lock (this.members)
                {
                    this.members.PrimaryKey = new DbColumnCollection(this.immutableProperties.PrimaryKeyColumnIndexes.Length);
                    foreach (int pkFieldIdx in this.immutableProperties.PrimaryKeyColumnIndexes)
                        this.members.PrimaryKey.Add(this.Columns[pkFieldIdx]);

                    this.members.PrimaryKey.MakeReadOnly();
                }
            }
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets catalog information to which the table/view belongs to.
        /// </summary>
        public ICatalog Catalog
        {
            get { return this.immutableProperties.Catalog; }
        }

        /// <summary>
        /// Gets table name.
        /// </summary>
        public string TableName
        {
            get { return this.immutableProperties.TableName; }
        }

        /// <summary>
        /// Gets object alias. By default it is equal to the table name.
        /// </summary>
        public string Alias
        {
            get { return this.alias ?? this.immutableProperties.TableName; }
        }

        /// <summary>
        /// Gets the value that specifies whether columns' aliases are prefixed and changed whenever the table alias is changed.
        /// </summary>
        /// <remarks>Usually columns aliases are prefixed with a table name/alias. This ensures the uniqueness 
        /// of column aliases when selecting two columns with the same name from two different tables.</remarks>
        public bool ColumnAliasesArePrefixed
        {
            get { return this.columnAliasesArePrefixed; }
        }

        /// <summary>
        /// Gets table fields.
        /// </summary>
        public DbColumnCollection Columns
        {
            get 
            {
                EnsureColumnsAreCreated();
                return this.members.Columns; 
            }
        }

        /// <summary>
        /// Gets primary key fields.
        /// </summary>
        public DbColumnCollection PrimaryKey
        {
            get
            {
                EnsurePrimaryKeyIsCreated();
                return this.members.PrimaryKey;
            }
        }

        /// <summary>
        /// Gets the names of the properties generated for the parent entities.
        /// </summary>
        public string[] ParentEntityProperties
        {
            get { return this.immutableProperties.ParentEntityProperties; }
        }

        /// <summary>Gets the collection of customized information associated with the table.</summary>
        /// <value>A IPropertyCollection with all custom information. <b>null</b> if no custom information is configured.</value>
        /// <remarks>The ExtendedProperties property lets you configure custom information for the table such as DBMS-specific properties.</remarks>
        public IPropertyCollection<string> ExtendedProperties
        {
            get { return this.immutableProperties.ExtendedProperties; }
        }

        /// <summary>Gets the value which indicates whether the database object is a view.</summary>
        /// <value><b>true</b> if object is a view; otherwise <b>false</b></value>
        public bool IsView
        {
            get { return this.immutableProperties.IsView; }
        }

        #endregion

        #region Methods.

        /// <summary>
        /// Gets the full property path for the specified parent entity field.
        /// </summary>
        /// <param name="fk">Relation to the parent entity.</param>
        /// <param name="parentColumn">Parent entity field. If null only property name generated for parent entity is returned.</param>
        /// <returns>Parent entity property name followed by dot operator and parent field property name if <b>parentColumn</b> is defined; otherwise only parent entity property name.</returns>
        /// <remarks>Eg. the following code snippet returns "RegionParent.RegionDescription" property path.
        /// <code>
        /// public string GetRegionDescriptionPropertyPath()
        /// {
        ///		TerritoriesMeta territories = new TerritoriesMeta();
        ///		RegionMeta regions = new RegionMeta();
        ///		return territories.GetParentProperty(territories.FK_RegionID, regions.RegionDescription);
        /// }
        /// </code>
        /// </remarks>
        public string GetParentProperty(DbRelation fk, IDbColumn parentColumn)
        {
            int idxParent = IndexOfRelation(this.ForeignKeys, fk);
            string fullPropertyName = (idxParent >= 0) ? GetParentProperty(idxParent, parentColumn) : null;
            return fullPropertyName;
        }

        private static int IndexOfRelation(DbRelation[] foreignKeys, DbRelation rel)
        {
            for (int idx = 0; idx < foreignKeys.Length; idx++)
            {
                if (rel.HasEqualForeignKeyFieldsAs(foreignKeys[idx]))
                    return idx;
            }

            return -1;
        }

        /// <summary>
        /// Gets the full property path for the specified parent entity field.
        /// </summary>
        /// <param name="parentIndex">Index of parent property name in the <see cref="ParentEntityProperties"/> array.</param>
        /// <param name="parentColumn">Parent entity field. If null only property name generated for parent entity is returned.</param>
        /// <returns>Parent entity property name followed by dot operator and parent field property name if <b>parentColumn</b> is defined; otherwise only parent entity property name.</returns>
        protected string GetParentProperty(int parentIndex, IDbColumn parentColumn)
        {
            string fullPropertyName = this.ParentEntityProperties[parentIndex];
            if (parentColumn != null)
                fullPropertyName += "." + parentColumn.PropertyName;

            return fullPropertyName;
        }

        /// <summary>Gets tables which reference the current table.</summary>
        /// <returns>Array of tables or empty array if the current table is not referenced by other objects.</returns>
        /// <remarks>The GetChildTables method must be implemented in derived class.</remarks>
        public virtual IDbTable[] GetChildTables()
        {
            throw new NotImplementedException(Messages.SealedDbTable_GetChildTablesNotImplemented);
        }

        /// <summary>Gets relations where current table acts as a parent.</summary>
        /// <returns>Array of relations or empty array if the current table is not referenced by other objects.</returns>
        /// <remarks>The GetChildRelations method must be implemented in derived class.</remarks>
        public virtual DbRelation[] GetChildRelations()
        {
            throw new NotImplementedException(Messages.SealedDbTable_GetChildRelationsNotImplemented);
        }

        #endregion

        #region Abstract New* and Clone methods.

        /// <summary>
        /// When implemented in inherited classes it should return a new empty entity compatibile with this IDbTable.
        /// </summary>
        /// <returns>New entity.</returns>
        public abstract IEntity NewEntity();

        /// <summary>
        /// When implemented in inherited classes it should return object that builds entities directly from retrieved data, ie. <see cref="IDataReader"/>.
        /// </summary>
        /// <returns>Entity filler.</returns>
        public abstract IObjectReader NewEntityReader();

        /// <summary>
        /// When implemented in inherited classes it should create another IDbTable object for the same table/view.
        /// </summary>
        /// <param name="cloneAlias">Clone alias. May be <b>null</b>.</param>
        /// <returns>Clone.</returns>
        public abstract IDbTable Clone(string cloneAlias);

        /// <summary>
        /// Creates another IDbTable object for the same table/view.
        /// </summary>
        /// <param name="cloneAlias">Clone alias.</param>
        /// <param name="setPrefixedAliases">Specifies whether cloned columns will have prefixed aliases.</param>
        /// <returns>Clone.</returns>
        public abstract IDbTable Clone(string cloneAlias, bool setPrefixedAliases);

        #endregion

        #region FK - template method.

        /// <summary>
        /// Initializes an array of relations used by foreign keys.
        /// Inherited classes must implement this method.
        /// The method is called when property <see cref="ForeignKeys"/> is accessed for the first time.
        /// </summary>
        /// <remarks>It is not wise to call this method in a constructor. If a recursive relation is defined, 
        /// or there is a circular path between multiple tables, an endless loop of CreateForeignKeys might occur. 
        /// It is best to call the method only when relations are required, i.e. accessed.</remarks>
        protected virtual DbRelation[] CreateForeignKeys()
        {
            DbRelation[] relations = new DbRelation[this.immutableProperties.ForeignKeys.Length];
            for (int i = 0; i < relations.Length; i++)
                relations[i] = new DbRelation(this.immutableProperties.ForeignKeys[i]);

            return relations;
        }

        private void EnsureForeignKeys()
        {
            if (this.members.ForeignKeys == null)
            {
                lock (this.members)
                {
                    this.members.ForeignKeys = CreateForeignKeys();
                }
            }
        }

        /// <summary>
        /// Gets the relations used by foreign keys.
        /// Empty array if no foreign key relations have been defined.
        /// </summary>
        public DbRelation[] ForeignKeys
        {
            get 
            {
                EnsureForeignKeys();
                return this.members.ForeignKeys; 
            }
        }

        #endregion
    }
}
